<?php
/**
 * Class to manage like
 *
 * @author 		Matt Mecham
 * @copyright	(c) 2001 - 2010 Invision Power Services, Inc.
 * @license		http://www.invisionpower.com/community/board/license.html
 * @package		Core
 * @link		http://www.invisionpower.com
 * @version		$Rev: 6651 $
 */

/* Factory, composite and cache classes all in one file to save requires
 * 
 * To use API-like methods:
 * $like = new classes_like( 'gallery', 'images' );
 * print $like->isLiked( $relId );

 * $html	= $like->render( 'summary', $relId );
 * print $html;
 * 
 * Matt, Joe, Bob and 5 others like this
 * 
 * 
 * This loads the app specific composite class (which inherits from the abstract class classes_like_composite
 * The composite class calls upon the registry and cache classes to manage data
 */

if ( ! defined( 'IN_IPB' ) )
{
	print "<h1>Incorrect access</h1>You cannot access this file directly. If you have recently upgraded, make sure you upgraded all the relevant files.";
	exit();
}

/**
 * Quick registry class for common methods
 *
 * @author matt
 */
class classes_like_registry
{
	/**
	 * App key
	 *
	 * @var string
	 */
	static private $app			= null;
	
	/**
	 * Area key
	 *
	 * @var string
	 */
	static private $area		= null;
	
	/**
	 * Total count
	 *
	 * @var string
	 */
	static private $count		= null;
	
	/**
	 * Cached bootstrap loaders
	 *
	 * @var	array
	 */
	static private $bootstraps	= array();
	
	/**
	 * Get app key
	 * 
	 * @return	string
	 */
	static public function getApp()
	{
		return self::$app;
	}
	
	/**
	 * Set app key
	 * 
	 * @param	string
	 * @return	void
	 */
	static public function setApp( $app )
	{
		self::$app = $app;
	}
	
	/**
	 * Get area key
	 * 
	 * @return string
	 */
	static public function getArea()
	{
		return self::$area;
	}
	
	/**
	 * Set area key
	 * 
	 * @param	string
	 * @return	void
	 */
	static public function setArea( $area )
	{
		self::$area = $area;
	}
	
	/**
	 * Get total count (saves on queries)
	 * 
	 * @return	string
	 */
	static public function getTotalCount()
	{
		return self::$count;
	}
	
	/**
	  * Set total count (saves on queries)
	 * 
	 * @param	string
	 * @return	void
	 */
	static public function setTotalCount( $count )
	{
		self::$count = $count;
	}
	
	/**
	 * Pack key. Either for cache table, or full table
	 *
	 * @param	int			relationship ID
	 * @param	int			member ID
	 * @return	binary		binary key
	 */
	static public function getKey( $relId, $memberId=null )
	{
		/* Check */
		if ( empty( $relId ) )
		{
			trigger_error( "Relationship ID missing in " . __CLASS__ . '::' . __FUNCTION__, E_USER_WARNING );
		}
		
		if ( $memberId === null )
		{
			/* For relative item only */
			return md5( self::$app . ';' . self::$area . ';' . $relId );
		}
		else if ( ! empty( $memberId ) )
		{
			/* For member speciifc relative item */
			return md5( self::$app . ';' . self::$area . ';' . $relId . ';' . $memberId );
		}
		else
		{
			trigger_error( "Member ID missing in " . __CLASS__ . '::' . __FUNCTION__, E_USER_WARNING );
		}
	}
	

}

/**
 * Simple static class for fetching meta data
 * @author matt
 *
 */
class classes_like_meta
{
	/**
	 * Retrieve the content title
	 *
	 * @param	array 		Like data
	 * @return	string		Title of content liked
	 */
	static public function getTitleFromId( $like )
	{
		if ( empty( $like['like_app'] ) OR empty( $like['like_area'] ) OR empty( $like['like_rel_id'] ) )
		{
			trigger_error( "You must have like_app, like_rel_id and like_area set", E_USER_WARNING );
		}
		
		$_bootstrap	= classes_like::bootstrap( $like['like_app'], $like['like_area'] );
		
		return $_bootstrap->getTitleFromId( $like['like_rel_id'] );
	}

	/**
	 * Retrieve the content URL
	 *
	 * @param	array 		Like data
	 * @return	string		URL of content liked
	 */
	static public function getUrlFromId( $like )
	{
		if ( empty( $like['like_app'] ) OR empty( $like['like_area'] ) OR empty( $like['like_rel_id'] ) )
		{
			trigger_error( "You must have like_app, like_rel_id and like_area set", E_USER_WARNING );
		}
		
		$_bootstrap	= classes_like::bootstrap( $like['like_app'], $like['like_area'] );
		
		return $_bootstrap->getUrlFromId( $like['like_rel_id'] );
	}
	
	/**
	 * Fetch all meta data from various like rows
	 * @param array $likes
	 * @return array
	 */
	static public function get( array $likes )
	{
		$apps    = array();
		$process = array();
		$lookUp  = array();
		
		foreach( $likes as $id => $like )
		{
			if ( empty( $like['like_app'] ) OR empty( $like['like_area'] ) OR empty( $like['like_rel_id'] ) )
			{
				continue;
			}
			
			$apps[ $like['like_app'] . '-' . $like['like_area'] ][ $id ] = $like;
			$lookUp[ $like['like_app'] . '-' . $like['like_area'] . '-' . $like['like_rel_id'] ] = $like['like_id'];
		}
		
		/* Process based on apps */
		foreach( $apps as $app => $data )
		{
			list( $_app, $_area ) = explode( '-', $app );
			
			$_bootstrap	= classes_like::bootstrap( $_app, $_area );
			
			$relIds = array();
			
			foreach( $data as $_k => $_v )
			{
				$relIds[] = $_v['like_rel_id'];
			}
		
			/* Bulk fetch */
			$process[ $app ] = $_bootstrap->getMeta( $relIds );
		}
		
		/* Fold up array */
		foreach( $process as $app => $data )
		{
			list( $_app, $_area ) = explode( '-', $app );
			
			foreach( $data as $relId => $_like )
			{
				if ( $relId )
				{
					$like_id = $lookUp[ $_app . '-' . $_area . '-' . $relId ];
					
					if ( $like_id AND is_array( $_like ) AND is_array( $likes[ $like_id ] ) )
					{
						$likes[ $like_id ] = array_merge( $likes[ $like_id ], $_like );
					}
				}
			}
		}
		
		return $likes;
	}
}

/**
 * Factory class, loads composite and child class
 *
 * @author matt
 */
class classes_like
{
	/**
	 * App object
	 *
	 * @var array
	 */
	static private $apps;
	
	/**
	 * Construct
	 *
	 * @param	string		Application
	 * @param	string		Area
	 * @return	string
	 */
	static public function bootstrap( $app=null, $area=null )
	{
		if ( $app === null OR $area === null )
		{
			trigger_error( "App or area missing from classes_like", E_USER_WARNING );
		}
		
		/* Pointless comment! */
		$_file	= IPSLib::getAppDir( $app ) . '/extensions/like/' . $area . '.php';
		$_class	= 'like_' . $app . '_' . $area . '_composite';
		$_key	= md5( $app . $area );
		
		/* Get from cache if already cached */
		if( isset( self::$apps[ $_key ] ) )
		{
			return self::$apps[ $_key ];
		}
		
		/* Otherwise create object and cache */
		if ( file_exists( $_file ) )
		{
			require_once( $_file );

			if ( class_exists( $_class ) )
			{
				classes_like_registry::setApp( $app );
				classes_like_registry::setArea( $area );
				
				self::$apps[ $_key ] = new $_class();
				self::$apps[ $_key ]->init();			
			}
			else
			{
				throw new Exception( "No like class available for $app - $area" );
			}
		}
		else
		{
			throw new Exception( "No like class available for $app - $area" );
		}
		
		return self::$apps[ $_key ];
	}
}

/**
 * Composite class. Holds main functionality.
 * 
 * @author matt
 */
abstract class classes_like_composite
{
	/**#@+
	 * Registry Object Shortcuts
	 *
	 * @var		object
	 */
	protected $registry;
	protected $DB;
	protected $settings;
	protected $request;
	protected $lang;
	protected $member;
	protected $memberData;
	protected $cache;
	protected $caches;
	/**#@-*/	
	
	/**
	 * Cache object
	 *
	 * @var	object
	 */
	protected $likeCache = null;
	
	/**
	 * App key
	 *
	 * @var	object
	 */
	protected $_app;
	
	/**
	 * App key
	 *
	 * @var	object
	 */
	protected $_area;
	
	/**
	 * Local settings object
	 *
	 * @var	array
	 */
	protected $_settings;
	
	/**
	 * Init. Yes, it is.
	 *
	 * @return	void
	 */
	public function init()
	{
		$this->_app  = classes_like_registry::getApp();
		$this->_area = classes_like_registry::getArea();
		
		if ( empty( $this->_app ) OR empty( $this->_area ) )
		{
			trigger_error( "Missing area or app variable in " . __CLASS__ . '::' . __FUNCTION__, E_USER_WARNING );
		}
		
		/* Fetch cache class */
		$this->likeCache = classes_like_cache::getInstance();
		
		/* Set a default cache expiration of an hour */
		$this->likeCache->setExpiration( 3600 );
	}
	
	/**
	 * Add a like
	 * 
	 * @param	int		Relationship ID
	 * @param	int		Member ID of user being added
	 * @param	array	Notification options
	 * @param	int		Anon flag
	 * @return	boolean
	 */
	public function add( $relId, $memberId, array $notifyOpts, $isAnon=false )
	{
		if ( empty( $relId ) OR empty( $memberId ) )
		{
			trigger_error( "Data missing in " . __CLASS__ . '::' . __FUNCTION__, E_USER_WARNING  );
		}
		
		/* first check to ensure we've not already like'd this item */
		if ( $this->isLiked($relId, $memberId) )
		{
			/* if any one cares to check, then we're all good */
			return false;
		}
		
		$notifyOpts = $this->_cleanNotifyOptions($notifyOpts);
		
		/* Save to deebee */
		$this->DB->insert( 'core_like', array(  'like_id'          => classes_like_registry::getKey( $relId, $memberId ),
											    'like_lookup_id'   => classes_like_registry::getKey( $relId ),
												'like_app'         => $this->_app,
												'like_area'        => $this->_area,
												'like_rel_id'      => $relId,
											 	'like_member_id'   => $memberId,
												'like_added'       => time(),
												'like_is_anon'	   => $isAnon,
												'like_notify_do'   => $notifyOpts['like_notify_do'],
												'like_notify_meta' => $notifyOpts['like_notify_meta'],
												'like_notify_freq' => ( $notifyOpts['like_notify_do'] ) ? $notifyOpts['like_notify_freq'] : '',
												'like_notify_sent' => 0 ) );
		
		/* Flag cache as stale */
		$this->likeCache->isNowStale( $relId );
		
		return true;
	}
	
	/**
	 * Send notifications to anyone subscribed to item
	 *
	 * @param	int		Relationship ID
	 * @param	array	Types of notifications to send (Possible: immediate, offline, daily, weekly)
	 * @param	array 	Notification options (Keys: notification_key, notification_url, email_template, email_subject, build_message_array, from (optional))
	 * @return	boolean
	 * @see		allowedFrequencies()
	 */
	public function sendNotifications( $relId, $type, $notificationOpts=array() )
	{
		if ( empty( $relId ) )
		{
			trigger_error( "Data missing in " . __CLASS__ . '::' . __FUNCTION__, E_USER_WARNING  );
		}
		
		$type	= is_array($type) ? $type : array( $type );

		//-----------------------------------------
		// Get users who have liked this item
		//-----------------------------------------
		
		$data = $this->getDataByRelationshipId( $relId );
		
		//-----------------------------------------
		// If no users, just return
		//-----------------------------------------

		if ( is_null($data) )
		{
			return false;
		}

		$classToLoad		= IPSLib::loadLibrary( IPS_ROOT_PATH . '/sources/classes/member/notifications.php', 'notifications' );
		$notifyLibrary		= new $classToLoad( $this->registry );
		
		//-----------------------------------------
		// Loop over members and send notifications
		//-----------------------------------------

		foreach( $data as $row )
		{
			//-----------------------------------------
			// Make sure this user is subscribed with this type
			//-----------------------------------------
			
			if( !$row['like_notify_freq'] OR !in_array( $row['like_notify_freq'], $type ) )
			{
				continue;
			}
			
			//-----------------------------------------
			// If "offline" type, make sure user is offline
			//-----------------------------------------
			
			if( $row['like_notify_freq'] == 'offline' AND $row['last_activity'] > ( time() - $this->settings['session_expiration'] ) )
			{
				continue;
			}

			//-----------------------------------------
			// Start building notification
			//-----------------------------------------
			
			IPSText::getTextClass('email')->getTemplate( $notificationOpts['email_template'], $row['language'] );

			//-----------------------------------------
			// Dynamically replace per-user data in message array
			//-----------------------------------------
			
			$buildMessage	= array();
			
			if ( is_array( $notificationOpts['build_message_array'] ) and count( $notificationOpts['build_message_array'] ) )
			{
				foreach( $notificationOpts['build_message_array'] as $k => $v )
				{
					if ( preg_match( '/\-member:(.+?)\-/', $v, $_matches ) )
					{
						$v = str_replace( $_matches[0], $row[$_matches[1]], $v );
					}
					
					$buildMessage[$k] = $v;
				}
			}

			IPSText::getTextClass('email')->buildMessage( $buildMessage );

			$notifyLibrary->setMember( $row );
			$notifyLibrary->setFrom( $notificationOpts['from'] ? $notificationOpts['from'] : $this->memberData );
			$notifyLibrary->setNotificationKey( $notificationOpts['notification_key'] );
			$notifyLibrary->setNotificationUrl( $notificationOpts['notification_url'] );
			$notifyLibrary->setNotificationText( IPSText::getTextClass('email')->message );
			$notifyLibrary->setNotificationTitle( $notificationOpts['email_subject'] );
			try
			{
				$notifyLibrary->sendNotification();
			}
			catch( Exception $e ){}
			
			//-----------------------------------------
			// Update sent timestamp
			//-----------------------------------------
			
			$this->DB->update( 'core_like', array( 'like_notify_sent' => time() ), 'like_id=\'' . classes_like_registry::getKey( $row['like_rel_id'], $row['like_member_id'] ) . '\'' );
		}
		
		return true;
	}
	
	/**
	 * Removes a like
	 * 
	 * @param	int		Relationship ID
	 * @param	int		Optional - If supplied, it'll remove that member rel. or it will remove all
	 * @param	array	Notification options
	 * @param	int		Anon flag
	 * @return	boolean
	 */
	public function remove( $relId, $memberId=null )
	{
		if ( empty( $relId ) )
		{
			trigger_error( "Data missing in " . __CLASS__ . '::' . __FUNCTION__, E_USER_WARNING  );
		}
		
		if ( $memberId === null )
		{
			/* Save to deebee */
			$this->DB->delete( 'core_like', 'like_lookup_id=\'' . classes_like_registry::getKey( $relId ) . '\'' );
		}
		else
		{
			/* Not liked? Save us the bother, then */
			if ( ! $this->isLiked($relId, $memberId) )
			{
				/* if any one cares to check, then we're all good */
				return false;
			}
					
			/* Save to deebee */
			$this->DB->delete( 'core_like', 'like_id=\'' . classes_like_registry::getKey( $relId, $memberId ) . '\'' );
		}
												
		/* Flag cache as stale */
		$this->likeCache->isNowStale( $relId );
		
		return true;
	}
	
	/**
	 * Render a view
	 * 
	 * @param	string	$view
	 * @param	int		$relId
	 * @param	array	Options
	 * @param	int		$memberId
	 * @return	string
	 */
	public function render( $view, $relId, $opts=array(), $memberId = null )
	{
		/* In the future we could abstract this out to its own class 
		 *  At this time we only have a few views so it would be overkill */
		if ( $view == 'summary' )
		{
			$data   = array( 'names' => array(), 'count' => 0, 'formatted' => '', 'iFave' => false );
			$cache  = $this->likeCache->get( $relId );
			
			/* We had the total count set from cache.get */
			if ( classes_like_registry::getTotalCount() )
			{
				/* fetch data */
				if ( $this->memberData['member_id'] AND $this->isLiked($relId, $this->memberData['member_id'] ) )
				{
					/* Now mix up the data */
					$data['names'][] = array( 'name' => $this->lang->words['fave_moi'], 'seo' => $this->memberData['members_seo_name'], 'id' => $this->memberData['member_id'] );
					
					/* Flag as me */
					$data['iFave'] = true;
				}
				
				if ( is_array( $cache['members'] ) )
				{
					$i   = 1;
					$max = 3;
					
					foreach( $cache['members'] as $mid => $mdata )
					{
						$_last = ( $i == $cache['count'] || $i == $max ) ? 1 : 0;
						
						/* Is this you? */
						if ( $mid == $this->memberData['member_id'] )
						{
							continue;
						}
						
						/* Push it on */
						$data['names'][] = array( 'name' => $mdata['n'], 'seo' => $mdata['s'], 'id' => $mid, 'last' => $_last );
						
						$i++;
						
						if ( $i > $max )
						{
							/* Done thanks */
							break;
						}
					}
				}
			}
			
			/* Finish off */
			$data['totalCount']  = $cache['count'];
			$data['othersCount'] = ( $cache['count'] > $max ) ? $cache['count'] - $max : 0;
			$data['app']		 = $this->_app;
			$data['area']		 = $this->_area;
			$data['formatted']   = $this->_formatNameString( $data );
			
			if ( IPS_IS_AJAX )
			{
				return $this->registry->output->getTemplate( $this->skin() )->likeSummaryContents( $data, $relId, $opts );
			}
			else
			{
				return $this->registry->output->getTemplate( $this->skin() )->likeSummary( $data, $relId, $opts );
			}
		}
		else if ( $view == 'more' )
		{
			/* Fetch members who have wanted to favorite this item */
			$data = $this->getDataByRelationshipId( $relId );
					
			return $this->registry->output->getTemplate( $this->skin() )->likeMoreDialogue( $data, $relId );
		}
	}
	
	
	/**
	 * Fetch form data for set dialogue
	 * 
	 * @param	int		relationship id
	 * @return	array
	 */
	public function getDataForSetDialogue( $relid )
	{
		$return = array( 'frequencies' => $this->allowedFrequencies(),
						 'notifyType'  => $this->getNotifyType() );
		
		return $return;
	}
	
	/**
	 * Has this user made this item a fave already dudes?
	 * 
	 * @param	int		Relationship ID
	 * @param	int		Member ID
	 * @return	boolean
	 */
	public function isLiked( $relId, $memberId )
	{
		/* Grab the data */
		return ( $this->getDataByMemberIdAndRelationshipId( $relId, $memberId ) === null ) ? false : true;
	}
	
	/**
	 * Get data based on a relationship ID and a member ID
	 *
	 * @param	mixed 	int $relId or array $relId (many)
	 * @param	int 	$memberId
	 * @return	array	Favorite data OR null
	 */
	public function getDataByMemberIdAndRelationshipId( $relId, $memberId )
	{
		$where = '';
		
		if ( is_numeric( $relId ) )
		{
			$where = 'like_id=\'' . classes_like_registry::getKey( $relId, $memberId ) . '\'';
		}
		else if ( is_array( $relId ) )
		{
			$relId = IPSLib::cleanIntArray( $relId );
			$keys  = array();
			
			foreach( $relId as $id )
			{
				$keys[] = "'" . classes_like_registry::getKey( $id, $memberId ) . "'";
			}
			
			if ( ! count( $keys ) )
			{
				return null;
			}
			
			$where = 'like_id IN (' . implode( ',', $keys ) . ')';
			
		}
		
		$this->DB->build( array( 'select' => '*',
					   			 'from'   => 'core_like',
								 'where'  => $where ) );
		
		$o = $this->DB->execute();
		
		while( $row = $this->DB->fetch( $o ) )
		{
			$data[ $row['like_rel_id'] ] = $row;
		}
		
		/* Just the one? */
		if ( is_numeric( $relId ) and count( $data ) )
		{
			$data = array_shift( $data );
		}
		
		return ( is_array( $data ) ) ? $data : null;
	}
	
	
	/**
	 * Get data based on a relationship ID
	 *
	 * @param	int 	$relId
	 * @return	mixed	Array of like data OR null
	 */
	public function getDataByRelationshipId( $relId )
	{
		/* Init */
		$mids	 = array();
		$members = array();
		$rows    = array();
		
		/* Fetch data */	
		$this->DB->build( array( 'select' => '*',
					   			 'from'   => 'core_like',
								 'where'  => 'like_lookup_id=\'' . classes_like_registry::getKey( $relId ) . '\' AND like_is_anon=0',
								 'order'  => 'like_added DESC',
								 'limit'  => array( 0, 250 ) ) );
		
		$o = $this->DB->execute();
		
		while( $row = $this->DB->fetch( $o ) )
		{
			$data[ $row['like_member_id'] ] = $row;
			$mids[ $row['like_member_id'] ] = intval( $row['like_member_id'] );
		}
		
		/* Just the one? */
		if ( count( $mids ) )
		{
			$members = IPSMember::load( $mids, 'all' );
		
			foreach( $members as $i => $d )
			{
				$_m = IPSMember::buildProfilePhoto( $d );
				$data[ $i ] = array_merge( (array) $_m, (array) $data[ $i ] );
			}
		}
		
		return ( is_array( $data ) ) ? $data : null;
	}
	
	/**
	 * Clean the notification options to make sure they're all nice and fluffy
	 * 
	 * @param 	array $notifyOpts
	 * @return 	array
	 */
	private function _cleanNotifyOptions( array $notifyOpts )
	{
		if ( isset( $notifyOpts['like_notify_do'] ) )
		{
			$notifyOpts['like_notify_do'] = intval( $notifyOpts['like_notify_do'] );
		}
		else
		{
			$notifyOpts['like_notify_do'] = 0;
		}
		
		if ( ! isset( $notifyOpts['like_notify_meta'] ) )
		{
			$notifyOpts['like_notify_meta'] = '';
		}
		
		if ( isset( $notifyOpts['like_notify_freq'] ) )
		{
			$notifyOpts['like_notify_freq'] = ( in_array( $notifyOpts['like_notify_freq'], $this->allowedFrequencies() ) ) ? $notifyOpts['like_notify_freq'] : '';
		}
		else
		{
			$notifyOpts['like_notify_freq'] = 0;
		}
		
		return $notifyOpts;
	}
	
    /**
	 * Return an array of acceptable frequencies
	 * Possible: immediate, offline, daily, weekly
	 * 
	 * @return	array
	 */
	public function allowedFrequencies()
	{
		return array( 'immediate', 'offline', 'daily', 'weekly' );
	}
	
	/**
	 * return type of notification available for this item
	 * 
	 * @return	array  	key, human readable
	 */
	public function getNotifyType()
	{
		return array( 'comments', 'comments' );
	}
	
	/**
	 * Fetch the template group
	 * 
	 * @return	string
	 */
	public function skin()
	{
		return 'global_other';
	}
	
	/**
	 * Return the title based on the passed id
	 * 
	 * @param	int		Relationship ID
	 * @return	string	Title
	 */
	public function getTitleFromId( $relId )
	{
		$meta = $this->getMeta( $relId, array( 'title' ) );
		
		if ( is_numeric( $relId ) )
		{
			return $meta[ $relId ]['like.title'];
		}
		else
		{
			$return = array();
			
			foreach( $meta as $id => $data )
			{
				$return[ $id ] = $data['like.title'];
			}
			
			return $return;
		}
	}

	/**
	 * Return the URL based on the passed id
	 * 
	 * @param	int		Relationship ID
	 * @return	string	URL
	 */
	public function getUrlFromId( $relId )
	{
		$meta = $this->getMeta( $relId, array( 'url' ) );
		
		if ( is_numeric( $relId ) )
		{
			return $meta[ $relId ]['like.url'];
		}
		else
		{
			$return = array();
			
			foreach( $meta as $id => $data )
			{
				$return[ $id ] = $data['like.url'];
			}
			
			return $return;
		}
	}
	
	/**
	 * Formats the Bob, Bill, Joe and 2038 Others Hate You
	 * 
	 * @param	array	$data
	 * @return	string
	 */
	private function _formatNameString( array $data )
	{
		$langString  = '';
		$seeMoreLink = 'app=core&amp;module=global&amp;section=like&amp;do=more';
		
		if ( ! is_array( $data['names'] ) OR ! count( $data['names'] ) )
		{
			return false;
		}
		/* Format up the names */
		$i      = 0;
		$_names = array();
		
		foreach( $data['names'] as $name )
		{
			if ( $this->memberData['member_id'] AND ( $this->memberData['member_id'] == $name['id'] ) )
			{
				$_names[$i] = $name['name'];
			}
			else
			{
				$_names[$i] = IPSLib::makeProfileLink($name['name'], $name['id'], $name['seo'] );
			}
			
			$i++;
		}

		/* More than one? */
		if ( $data['totalCount'] > 1 )
		{
			/* Joe and Matt love you */
			if ( $data['totalCount'] == 2 )
			{
				$_n = $_names[0] . ' ' . $this->lang->words['fave_and'] . ' ' . $_names[1];
				
				$langString = sprintf( $this->lang->words['fave_formatted_many'], $_n );
			}
			/* Joe, Matt and Mike love you more */
			else if ( $data['totalCount'] == 3 )
			{
				$_n = $_names[0] . ', ' . $_names[1] . ' ' . $this->lang->words['fave_and'] . ' ' . $_names[2];
				
				$langString = sprintf( $this->lang->words['fave_formatted_many'], $_n );
			}
			/* Joe, Matt, Mike and 1 more love you */
			else if ( $data['totalCount'] == 4 )
			{
				$_n = $_names[0] . ', ' . $_names[1] . ' ' . $this->lang->words['fave_and'] . ' ' . $_names[2];
				
				$langString = sprintf( $this->lang->words['fave_formatted_one_more'], $_n, $seeMoreLink );
			}
			/* Joe, Matt, Mike and 5 more are indifferent to your redonkulous comments */
			else
			{
				$_n = $_names[0] . ', ' . $_names[1] . ', ' . $_names[2];
				
				$langString = sprintf( $this->lang->words['fave_formatted_more'], $_n, $seeMoreLink, $data['othersCount'] );
			}
		}
		else
		{
			/* Just the one and it might be you! */	
			if ( $data['names'][0]['id'] == $this->memberData['member_id'] )
			{
				$langString = $this->lang->words['fave_formatted_me'];
			}
			else
			{
				$langString = sprintf( $this->lang->words['fave_formatted_one'], $_names[0] );
			}
		}

		return $langString;
	}
}


/**
 * Favorites cache class
 * 
 * @author matt
 */
class classes_like_cache
{
	/**#@+
	 * Registry Object Shortcuts
	 *
	 * @var		object
	 */
	protected $registry;
	protected $DB;
	protected $settings;
	protected $request;
	protected $lang;
	protected $member;
	protected $cache;
	/**#@-*/	
	
	/**
	 * Cache expiration
	 *
	 * @var int
	 */
	protected $_expire = 0;
	
	/**
	 * App key
	 *
	 * @var	object
	 */
	protected $_app;
	
	/**
	 * App key
	 *
	 * @var	object
	 */
	protected $_area;
	
	/**
	 * Instance of an object
	 *
	 * @var object
	 */
	private static $instance = null;
	
	/**
	 * Singleton
	 *
	 * @return	object
	 */
	public static function getInstance()
	{
		if ( self::$instance === null )
		{
			self::$instance = new self();
		}
		
		return self::$instance;
	}
	
	/**
	 * Our desctructor. Just kidding.
	 * 
	 * @return void
	 */
	public function __construct()
	{
		/* Fetch registry like a good boy */
		$this->registry   =  ipsRegistry::instance();
		$this->DB         =  $this->registry->DB();
		$this->settings   =& $this->registry->fetchSettings();
		$this->request    =& $this->registry->fetchRequest();
		$this->lang       =  $this->registry->getClass('class_localization');
		$this->member     =  $this->registry->member();
		$this->memberData =& $this->registry->member()->fetchMemberData();
		$this->cache      =  $this->registry->cache();
		$this->caches     =& $this->registry->cache()->fetchCaches();
		
		$this->_app  = classes_like_registry::getApp();
		$this->_area = classes_like_registry::getArea();
		
		if ( empty( $this->_app ) OR empty( $this->_area ) )
		{
			trigger_error( "Missing area or app variable in " . __CLASS__ . '::' . __FUNCTION__, E_USER_WARNING );
		}
	}
	
	/**
	 * Set the cache expiration
	 *
	 * @param	int		$seconds
	 * @return	void
	 */
	public function setExpiration( $seconds )
	{
		$this->_expire = intval( $seconds );
	}
	
	/**
	 * Fetch an item from the cache
	 * 
	 * @param	int		$relId
	 * @param	int		$memberId
	 * @return	array
	 */
	public function get( $relId, $memberId = null )
	{
		/* Possible future expansion */
		if ( $memberId === null )
		{
			$cache = $this->DB->buildAndFetch( array( 'select' => '*',
													  'from'   => 'core_like_cache',
													  'where'  => 'like_cache_id=\'' .  classes_like_registry::getKey( $relId ) . '\'' ) );
			
			if ( ! is_array( $cache ) OR ( $cache['like_cache_expire'] <= time() ) )
			{
				/* We don't have a valid cache, so create one and return the data */
				$cache = $this->create( $relId, $memberId );
			}
			
			$tmp = unserialize( $cache['like_cache_data'] );
			$cache['members'] = $tmp['members'];
			$cache['count']   = intval( $tmp['count'] );
			
			/* Set total count for use elsewhere */
			classes_like_registry::setTotalCount( $cache['count'] );
			
			return $cache;
		}
	}
	
	/**
	 * Creates/updates the relationship ID's cache.
	 * 
	 * @param	int		$relId
	 * @param	int		$memberId
	 * @return	array	Array of data stored
	 */
	public function create( $relId, $memberId = null )
	{
		$items   = array();
		$members = array();
		$mids	 = array();
		$data    = array( 'members' => null, 'count' => 0 );
		
		/* Possible future expansion */
		if ( $memberId === null )
		{
			/* Count all first */
			$cou = $this->DB->buildAndFetch( array( 'select' => 'COUNT(*) as nt',
									 				'from'   => 'core_like',
									 				'where'  => 'like_lookup_id=\'' . classes_like_registry::getKey( $relId ) . '\' AND like_is_anon=0' ) );
			
			$data['count'] = intval( $cou['nt'] );
			
			/* Fetch all items for this app:key:$relId */
			$this->DB->build( array( 'select' => '*',
									 'from'   => 'core_like',
									 'where'  => 'like_lookup_id=\'' . classes_like_registry::getKey( $relId ) . '\' AND like_is_anon=0',
									 'order'  => 'like_added DESC',
									 'limit'  => array( 0, 5 )  ) );
			
			$o = $this->DB->execute();
			
			while( $row = $this->DB->fetch($o) )
			{
				$items[ $row['like_rel_id'] ]   = $row;
				$mids[ $row['like_member_id'] ] = $row['like_member_id'];
			}
			
			/* Load members */
			$members = IPSMember::load( $mids, 'core' );
			
			/* We don't need all the member's infos */
			foreach( $members as $mid => $mdata )
			{
				$data['members'][ $mid ] = array( 'n' => $mdata['members_display_name'], 's' => $mdata['members_seo_name'] );
			}
		
			/* Build save array */
			$store = array( 'like_cache_id'     => classes_like_registry::getKey( $relId ),
						    'like_cache_app'    => $this->_app,
						    'like_cache_area'   => $this->_area,
						    'like_cache_rel_id' => $relId,
						    'like_cache_data'   => serialize( $data ),
						    'like_cache_expire' => time() + $this->_expire );
			
			/* Update the cache */
			$this->DB->replace( 'core_like_cache', $store, array( 'like_cache_id' ) );
						
			return $store;
		}		
		
		/* We know there's a previous cache row, so delete it */
		$this->DB->delete( 'core_like_cache', 'like_cache_id=\'' . classes_like_registry::getKey( $relId ) . '\'' );
	}
	
	/**
	 * Deletes a cache.
	 * 
	 * @param	int 	$relId
	 * @param	int		$memberId
	 * @return	void
	 */
	public function delete( $relId, $memberId = null )
	{
		/* Possible future expansion */
		if ( $memberId === null )
		{
			$this->DB->delete( 'core_like_cache', 'like_cache_id=\'' . classes_like_registry::getKey( $relId ) . '\'' );
		}
	}
	
	/**
	 * Flags a cache as stale. We choose to delete, but the cache class should make the call
	 * not the application
	 * 
	 * @param	int		$relId
	 * @param	int		$memberId
	 * @return	void
	 */
	public function isNowStale( $relId, $memberId = null )
	{
		return $this->delete( $relId, $memberId );
	}
}